import JSON
import Mustache

mutable struct NotebookOutput end

mutable struct MarkdownOutput end

mutable struct NowebOutput end

mutable struct ScriptOutput end

const output_formats = Dict{String,Any}(
    "noweb" => NowebOutput(),
    "notebook" => NotebookOutput(),
    "markdown" => MarkdownOutput(),
    "script" => ScriptOutput(),
)

"""Autodetect format for converter"""
function detect_outformat(outfile::String)
    ext = lowercase(splitext(outfile)[2])

    ext == ".jl" && return "script"
    ext == ".jmd" && return "markdown"
    ext == ".ipynb" && return "notebook"
    return "noweb"
end

"""
    convert_doc(infile::AbstractString, outfile::AbstractString; format::Union{Nothing,AbstractString} = nothing)

Convert Weave documents between different formats

- `infile`: Path of the input document
- `outfile`: Path of the output document
- `format = nothing`: Output document format (optional). It will be detected automatically from the `outfile` extension. You can also specify either of `"script"`, `"markdown"`, `"notebook"`, or `"noweb"`
"""
function convert_doc(
    infile::AbstractString,
    outfile::AbstractString;
    format::Union{Nothing,AbstractString} = nothing,
)
    doc = WeaveDoc(infile)

    isnothing(format) && (format = detect_outformat(outfile))

    converted = convert_doc(doc, output_formats[format])

    open(outfile, "w") do f
        write(f, converted)
    end
end

"""Convert Weave document to Jupyter notebook format"""
function convert_doc(doc::WeaveDoc, format::NotebookOutput)
    nb = Dict()
    nb["nbformat"] = 4
    nb["nbformat_minor"] = 2
    metadata = Dict()
    kernelspec = Dict()
    kernelspec["language"] = "julia"
    kernelspec["name"] = "julia-$(VERSION.major).$(VERSION.minor)"
    kernelspec["display_name"] = "Julia $(VERSION.major).$(VERSION.minor).$(VERSION.patch)"
    metadata["kernelspec"] = kernelspec
    language_info = Dict()
    language_info["file_extension"] = ".jl"
    language_info["mimetype"] = "application/julia"
    language_info["name"] = "julia"
    language_info["version"] = "$(VERSION.major).$(VERSION.minor).$(VERSION.patch)"
    metadata["language_info"] = language_info
    cells = []
    ex_count = 1

    # Handle header
    head_tpl = """
    {{#:title}}# {{:title}}{{/:title}}
    {{#:author}}### {{{:author}}}{{/:author}}
    {{#:date}}### {{{:date}}}{{/:date}}
    """

    if isa(doc.chunks[1], DocChunk)
        strip_header!(doc)
        doc.chunks[1].content[1].content =
            Mustache.render(head_tpl; [Pair(Symbol(k), v) for (k, v) in doc.header]...) * doc.chunks[1].content[1].content
    end

    for chunk in doc.chunks
        if isa(chunk, DocChunk)
            push!(
                cells,
                Dict(
                    "cell_type" => "markdown",
                    "metadata" => Dict(),
                    "source" => [strip(join([repr(c) for c in chunk.content], ""))],
                ),
            )
        elseif haskey(chunk.options, :skip) && chunk.options[:skip] == "notebook"
            continue
        else
            push!(
                cells,
                Dict(
                    "cell_type" => "code",
                    "metadata" => Dict(),
                    "source" => [strip(chunk.content)],
                    "execution_count" => nothing,
                    "outputs" => [],
                ),
            )
        end
    end

    nb["cells"] = cells
    nb["metadata"] = metadata

    json_nb = JSON.json(nb, 2)
    return json_nb
end

"""Convert Weave document to Jupyter notebook format"""
function convert_doc(doc::WeaveDoc, format::MarkdownOutput)
    output = ""
    for chunk in doc.chunks
        if isa(chunk, DocChunk)
            output *= join([repr(c) for c in chunk.content], "")
        else
            output *= "\n" * "```julia"
            isempty(chunk.optionstring) || (output *= ";" * chunk.optionstring)
            output *= "\n" * lstrip(chunk.content)
            output *= "```\n"
        end
    end

    return output
end

"""Convert Weave document to noweb format"""
function convert_doc(doc::WeaveDoc, format::NowebOutput)
    output = ""
    for chunk in doc.chunks
        if isa(chunk, DocChunk)
            output *= join([repr(c) for c in chunk.content], "")
        else
            output *= "\n" * "<<"
            isempty(chunk.optionstring) || (output *= strip(chunk.optionstring))
            output *= ">>="
            output *= "\n" * lstrip(chunk.content)
            output *= "@\n"
        end
    end

    return output
end

"""Convert Weave document to script format"""
function convert_doc(doc::WeaveDoc, format::ScriptOutput)
    output = ""
    for chunk in doc.chunks
        if typeof(chunk) == Weave.DocChunk
            content = join([repr(c) for c in chunk.content], "")
            output *= join(["#' " * s for s in split(content, "\n")], "\n")
        else
            output *= "\n#+ "
            isempty(chunk.optionstring) || (output *= strip(chunk.optionstring))
            output *= "\n\n" * lstrip(chunk.content)
            output *= "\n"
        end
    end

    return output
end

function Base.repr(c::InlineText)
    return c.content
end

function Base.repr(c::InlineCode)
    return "`j $(c.content)`"
end
